iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Modern Web

起步Go!Let's Go!系列 第 11

[ Day 11] Go 指標與記憶體魔法

  • 分享至 

  • xImage
  •  

什麼是指標?

是一種在程式語言中用於儲存記憶體位址的特殊變數或數據型態。指標允許你直接訪問和操作記憶體中的數據,而不是僅存儲數據的值。
在許多程式語言中,包括 Go,在記憶體中的每個變數或數據都有一個唯一的位址,指標就是用來引用這些位址的。
簡單來說,指摽就是用來儲存資料的記憶體位址。

先前提到的變數都是把右邊的值存進左邊的變數中,而這些變數其實就在記憶體中。而記憶體都會有「位址」,這一章節要將這些變數所在的位址提取出來。
再開始介紹指標之前先看看它大概長怎樣:

package main
import "fmt"
func main(){
    num := 10
    pointer := &num
    fmt.Println(pointer)
}

執行結果
0x140000b4008

注意!!
每一台電腦所輸出的結果都不會一樣。

小知識:
地址是一串數字為什麼會出來數字和英文?在程式語言中以 0x 開頭的數字代表是 16 進位0~f 組成,跟一般我們常用的 10 進位不太一樣。我們的電腦是由 0 和 1 的 2 進位系統組成,因為 2 進位16 進位之間比較好轉換 ,所以比起 10 進位,工程師更常使用 16 進位

基本思考順序

當我們在談指標時,談的不是一件事,而是一套流程。
這套流程的思考順序如下:

  1. 建立資料變數
    首先會建立一個變數去存放資料。
  2. 取得記憶體位址
    接著會試圖去取得這個資料的記憶體位址,取得之後就要想,有沒有需要把這個資料的記憶體位址存放起來。
  3. 存放到指標變數
    如果有需要把這個記憶體位址存放起來,那就要建立一個指標變數,將其存放起來。而這個指標變數專門存放這個資料的記憶體位址。
    例如:
    整數變數用來存放一個整數
    整數的指標變數用來存放整數的記憶體位址
  4. 反解指摽變數
    有時候,我們有一個指標變數,那我需要知道他之前的對應的資料是什麼,這時候就需要做反解,最後取得原始的資料。

接下來一一來介紹以上的流程:

建立資料變數

也就是宣告變數及資料型態,如下:

var x int = 3

其實我們也可以簡化宣告變數的寫法:

x := 3

取得記憶體位址

可以取得這個資料變數的記憶體位址,要怎麼取得,如下:

var x int = 3
// 取得記憶體位址:&變數名稱
fmt.Println(&x)

取得記憶體位址很簡單,只要 &變數名稱,它就會告訴我它放在記憶體位址的哪個地方。
我們的每一個資料都會被放在記憶體的某個地方。
舉一個生活中的例子:
每棟房子都會有那棟房子專屬的地址,而資料也是。
房子地址就會是資料中的記憶體位址
房子就會是資料

存放在指標變數

有沒有需要把取得的記憶體位址存放起來,如果需要,就要建立一個指標變數,建立指摽變數的方法如下:

package main
import "fmt"
func main() {
	var x int = 3
	// 指標資料型態: *資料型態
	var xPtr *int = &x
	fmt.Println(xPtr)
}

執行結果:
0xc000012028

資料變數與指標變數的差異在於它的資料型態,所以 int 就是整數的指標。
所以一開始的資料型態是 int 那指標資料型態就會是 intstring 就會是 string
&x 就會被放入 xPtr 這個指標變數中。
印出來會是記憶體位址。

反解指標變數

有時候,我們會有指標變數,想要知道對應的指標變數對應的資料是什麼,這時候就需要反解指標變數,去取得原始的資料。

package main
import "fmt"
func main() {
	var x int = 3
	// 指標資料型態: *資料型態
	var xPtr *int = &x
	fmt.Println(*xPtr)
}

執行結果:
3

注意!!
這邊要特別注意不要把 *指標資料型態,搞混成 *指標變數名稱

程式範例

package main
import "fmt"
func main() {
    // 1. 建立存放資料的變數
    var x int = 5
    fmt.Println("原來的資料:", x)
    // 2. 取得記憶體位址:&變數名稱
    fmt.Println("資料的記憶體位址:", &x)
    // 3. 存放到指標變數,注意指標資料型態:*資料型態
    var xPtr *int = &x
    fmt.Println("資料的記憶體位址:", xPtr)
    // 4. 反解指標變數:*指標變數名稱
    fmt.Println("反解指標回原來的資料:", *xPtr)
}

執行結果:
原來的資料: 5
資料的記憶體位址: 0xc000012028
資料的記憶體位址: 0xc000012028 (這邊會印出跟 2 一樣的資料,因為將資料的記憶體位址存放在 xPtr 的指摽變數中)
反解指標回原來的資料: 5

指標練習

package main
import "fmt"
func main() {
    // 1. 建立存放資料的變數
    var word string = "Hello"
    fmt.Println("原來的資料:", word)
    // 2. 取得記憶體位址:&變數名稱
    fmt.Println("資料的記憶體位址:", &word)
    // 3. 存放到指標變數,注意指標資料型態:*資料型態
    var wordPtr *string = &word
    fmt.Println("資料的記憶體位址:", wordPtr)
    // 4. 反解指標變數:*指標變數名稱
    fmt.Println("反解指標回原來的資料:", *wordPtr)
}

執行結果:
原來的資料: Hello
資料的記憶體位址: 0xc000014070
資料的記憶體位址: 0xc000014070
反解指標回原來的資料: Hello

所謂的反解指摽回原來的資料,也就是將字串指標在變回原本的字串,所以這邊也可以這樣寫:

package main
import "fmt"
func main() {
    var word string = "Hello"
    fmt.Println("原來的資料:", word)
    fmt.Println("資料的記憶體位址:", &word)
    var wordPtr *string = &word
    fmt.Println("資料的記憶體位址:", wordPtr)
    word = *wordPtr
    fmt.Println("反解指標回原來的資料:", word)
}

也可以用反解指摽變數去存在原本的變數。
或是在宣告一個新的變數去存放反解指標變數,這樣也可以。

package main
import "fmt"
func main() {
    var word string = "Hello"
    fmt.Println("原來的資料:", word)
    fmt.Println("資料的記憶體位址:", &word)
    var wordPtr *string = &word
    fmt.Println("資料的記憶體位址:", wordPtr)
    var another string = *wordPtr
    fmt.Println("反解指標回原來的資料:", another)
}

利用指標變數更改指向變數的值

我們可以利用指標特性直接更改其指向變數的值。

package main
import "fmt"
func main(){
    num := 10
    p := &num
    fmt.Println("更改前的 num =",num)
    *p = 4450
    fmt.Println("更改後的 num =",num)
    fmt.Println("p = ", &num)
    fmt.Println("*p = ", *p)
}

執行結果:
更改前的 num = 10
更改後的 num = 4450
p = 0xc000012028
*p = 4450

利用指標的特性,透過更改該指標變數的方式更改指向變數的值。

利用非指標更改另一個變數的值

既然可以用指標變數去更改變數的值,那麼是不是可以用非指標變數去更改另一個變數的值?
答案是不行!!!

package main
import "fmt"
func main(){
    num := 10
    num2 := num
    num2 = 20
    fmt.Println("num2 =", num2)
    fmt.Println("num =", num)
}

執行結果:
num2 = 20
num = 10

第 5 行嘗試複製 num,但這時已經將 num2 放到新的記憶體空間,並將其值放進去,所以之後去做更改,也不會更改到 num

我們印出兩個變數的記憶體位址出來:

package main
import "fmt"
func main(){
    num := 10
    num2 := num
    num2 = 20
    fmt.Println("num =", num)
    fmt.Println("pointer of num =", &num)
    fmt.Println("num2 =", num2)
    fmt.Println("pointer of num2 =", &num2)
}

執行結果:
num = 10
pointer of num = 0x1400001c0c8
num2 = 20
pointer of num2 = 0x1400001c0e0

印出來的結果,可以看到兩個變數的位址都不一樣,所以不能透過非指標變數去更改另一個變數的值。

參考資料:

  1. Golang 指標基礎 Pointer - 記憶體位址、指標變數與資料型態、反解指標 By 彭彭

上一篇
[ Day 10] Go 函式中的魔法 Return
下一篇
[ Day 12] Go 指標參數:釋放函式的潛力
系列文
起步Go!Let's Go!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言